package io.gitee.devaper.starlight.security.config

import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.module.kotlin.*
import io.gitee.devaper.starlight.*
import io.gitee.devaper.starlight.common.constant.*
import io.gitee.devaper.starlight.common.mapper.*
import io.gitee.devaper.starlight.security.*
import io.gitee.devaper.starlight.security.common.*
import io.gitee.devaper.starlight.security.handler.*
import jakarta.servlet.http.*
import org.slf4j.*
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.*
import org.springframework.data.redis.core.*
import org.springframework.http.*
import org.springframework.http.converter.json.*
import org.springframework.security.config.annotation.*
import org.springframework.security.config.annotation.web.builders.*
import org.springframework.security.config.annotation.web.configuration.*
import org.springframework.security.config.annotation.web.configurers.*
import org.springframework.security.core.*
import org.springframework.security.core.session.*
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.*
import org.springframework.security.crypto.password.*
import org.springframework.security.jackson2.*
import org.springframework.security.web.*
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
import org.springframework.security.web.savedrequest.*
import org.springframework.security.web.session.*
import org.springframework.web.cors.*
import java.nio.charset.StandardCharsets
import kotlin.reflect.*

private typealias SecurityConfigurerClass = Class<SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity>>

/**
 * SpringSecurity配置类
 */
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SecurityProperties::class)
open class SecurityConfiguration(
    private val securityProperties: SecurityProperties
) {

    /**
     * 并发会话中Session对应事件的发布器
     */
    @Bean
    open fun httpSessionEventPublisher() = HttpSessionEventPublisher()

    @Bean
    open fun passwordEncoder() = BCryptPasswordEncoder()

    @Suppress("unchecked_cast")
    @Bean
    open fun securityFilterChain(
        @Qualifier(SECURITY_REDIS_TEMPLATE_BEAN_NAME) redisTemplate: RedisTemplate<String, Any>,

        httpSecurity: HttpSecurity,
        userDetailsService: UserDetailsService,

        authenticationSuccessHandler: AuthenticationSuccessHandler,
        authenticationFailureHandler: AuthenticationFailureHandler,
        authenticationEntryPoint: AuthenticationEntryPoint,
        logoutSuccessHandler: LogoutSuccessHandler,

        sessionRegistry: SessionRegistry,
        sessionInformationExpiredStrategy: SessionInformationExpiredStrategy,
        concurrentSessionControlAuthenticationStrategy: ConcurrentSessionControlAuthenticationStrategy
    ): SecurityFilterChain {

        // 移除掉默认表单登录
        httpSecurity.removeConfigurer(DefaultLoginPageConfigurer::class.java as SecurityConfigurerClass)

        val concurrentSessionAuthenticationStrategyComposite = SessionAuthenticationStrategyComposite(
            concurrentSessionControlAuthenticationStrategy,
            RegisterSessionAuthenticationStrategy(sessionRegistry)
        )

        return httpSecurity
            .authorizeHttpRequests {
                it.anyRequest().authenticated()
            }
            .formLogin {
                it.loginProcessingUrl(HTTP_URL_PREFIX + HTTP_URL_LOGIN)
                    .successHandler(authenticationSuccessHandler)
                    .failureHandler(authenticationFailureHandler)
            }
            .logout {
                it.logoutUrl(HTTP_URL_PREFIX + HTTP_URL_LOGOUT)
                    .invalidateHttpSession(true)
                    .clearAuthentication(true)
                    .addLogoutHandler(LogoutSuccessEventPublishingLogoutHandler())
                    .logoutSuccessHandler(logoutSuccessHandler)
            }
            .sessionManagement {
                it.sessionFixation().changeSessionId()
                // 自定义并发会话的SessionAuthenticationStrategy
                it.sessionAuthenticationStrategy(concurrentSessionAuthenticationStrategyComposite)
                // 手动添加ConcurrentSessionFilter, 上面自定义了SessionAuthenticationStrategy就会导致不自动添加ConcurrentSessionFilter
                httpSecurity.addFilter(ConcurrentSessionFilter(sessionRegistry, sessionInformationExpiredStrategy).apply {
                    // 将LogoutConfigurer的logoutHandler设置进来, 确保并发会话中登出的用户也享有所有的LogoutHandler
                    // 这里拿到的logoutHandlers是一个空集合, 还没有任何值, 传递给ConcurrentSessionFilter时它直接将集合赋值给了自己, 所以可以利用引用的特性来使其获得到后续添加的所有Handler
                    setLogoutHandlers(httpSecurity.getConfigurer(LogoutConfigurer::class.java as Class<LogoutConfigurer<HttpSecurity>>).logoutHandlers)
                })
            }
            .rememberMe {
                val config = securityProperties.remember
                if (config.enable) {
                    it.rememberMeServices(RedisConcurrentSessionControlRememberMeServices(
                        key = config.key,
                        secret = config.secret.toByteArray(StandardCharsets.UTF_8),
                        userDetailsService = userDetailsService,
                        redisOperations = redisTemplate
                    ).apply {
                        parameter = config.parameterName
                        setAlwaysRemember(config.always)
                        setCookieName(config.cookieName)
                        setUseSecureCookie(config.secure)
                        setTokenValiditySeconds(config.timeout.toSeconds().toInt())
                    })
                } else {
                    it.disable()
                }
            }
            .exceptionHandling {
                it.authenticationEntryPoint(authenticationEntryPoint)
            }
            // csrf/requestCache会产生大量的session所以禁用掉, 现在只保证登录成功后才会创建session, 减少垃圾资源的产生
            .csrf { it.disable() }
            .cors { it.disable() }
            .requestCache { it.disable() }
            .build()
    }




}